/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is Forte for Java, Community Edition. The Initial * Developer of the Original Code is Sun Microsystems, Inc. Portions * Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved. */ package org.openide.loaders; import java.net.URL; import java.io.*; import java.util.*; import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.text.MessageFormat; import org.xml.sax.*; import org.xml.sax.helpers.*; import org.w3c.dom.Document; import org.openide.*; import org.openide.actions.*; import org.openide.cookies.EditorCookie; import org.openide.filesystems.*; import org.openide.loaders.*; import org.openide.text.*; import org.openide.util.*; import org.openide.util.actions.SystemAction; import org.openide.nodes.Node; import org.openide.nodes.Children; import org.openide.nodes.CookieSet; /** Object that provides main functionality for xml data loader. * * @author Libor Kramolis, Jaroslav Tulach */ public class XMLDataObject extends MultiDataObject { /** Public ID of xmlinfo dtd. */ public static final String XMLINFO_DTD_PUBLIC_ID = "-//Forte for Java//DTD xmlinfo//EN"; // NOI18N public static final String XMLINFO_DTD_PUBLIC_ID_OLD = "-//NetBeans IDE//DTD xmlinfo//EN"; // NOI18N /** the static instance for users that do not want to have own processor */ private static RequestProcessor XML_RP = new RequestProcessor ("XMLDataObject::Info"); // NOI18N /** Not parsed yet. Constant for getStatus method. */ public static final int STATUS_NOT = 0; /** Parsed ok. Constant for getStatus method. */ public static final int STATUS_OK = 1; /** Parsed with warnings. Constant for getStatus method. */ public static final int STATUS_WARNING = 2; /** Parsed with errors. Constant for getStatus method. */ public static final int STATUS_ERROR = 3; /** property name of document property */ public static final String PROP_DOCUMENT = "document"; // NOI18N /** property name of info property */ public static final String PROP_INFO = "info"; // NOI18N /** generated Serialized Version UID */ static final long serialVersionUID = 8757854986453256578L; /** Task for parse xmlinfo file */ private Task infoTask = Task.EMPTY; /** xmlinfo */ private Info info; /** XmlDocument created from 'xml' file * Weaker reference to org.w3c.dom.Document */ private Reference xmlDocument = new SoftReference (null); /** XML parse error handler */ static private ErrorPrinter errorHandler = new ErrorPrinter(); /** XML entity resolver */ static private com.sun.xml.parser.Resolver entityResolver = new com.sun.xml.parser.Resolver(); /** the result of parsing */ private int status; /** editor support */ private EditorCookie editor = createEditorCookie(); /** parser of xmlinfo, watcher over changes of documents */ private InfoParser infoParser = new InfoParser (); static { entityResolver.setIgnoringMIME (true); registerCatalogEntry (XMLINFO_DTD_PUBLIC_ID, "org/openide/resources/xmlinfo.dtd", // NOI18N ClassLoader.getSystemClassLoader()); registerCatalogEntry (XMLINFO_DTD_PUBLIC_ID_OLD, // [temporary] - back compability "org/openide/resources/xmlinfo.dtd", // NOI18N ClassLoader.getSystemClassLoader()); } /** Create new XMLDataObject * * @param fo the primary file object * @param loader loader of this data object */ public XMLDataObject (FileObject fo, MultiFileLoader loader) throws DataObjectExistsException { super (fo, loader); fo.getParent ().addFileChangeListener ( WeakListener.fileChange (infoParser, fo.getParent ()) ); status = STATUS_NOT; info = null; fo = FileUtil.findBrother (fo, Loader.XMLINFO_EXT); registerEntry (fo); infoParser.setInfoFile (fo); } /** Provides node that should represent this data object. When a node for representation * in a parent is requested by a call to getNode (parent) it is the exact copy of this node * with only parent changed. This implementation creates instance * <CODE>DataNode</CODE>. * <P> * This method is called only once. * * @return the node representation for this data object * @see DataNode */ protected Node createNodeDelegate () { DataNode node = new DataNode (this, Children.LEAF); node.setIconBase ("/org/openide/resources/xmlObject"); // NOI18N node.setDefaultAction (SystemAction.get (OpenAction.class)); return node; } /** Called when the info file is parsed and the icon should change. * @param res resource for the icon */ protected void updateIconBase (String res) { DataNode node = (DataNode)getNodeDelegate(); node.setIconBase (res); } public HelpCtx getHelpCtx () { return new HelpCtx (XMLDataObject.class); } /** @return a cookie if it has been found in the xmlinfo file */ public Node.Cookie getCookie (Class cls) { infoTask.waitFinished(); return super.getCookie (cls); } /** Allows subclasses to provide own editor cookie. * @return editor cookie to use */ protected EditorCookie createEditorCookie () { return new EditorSupport (getPrimaryEntry()); } /** Creates w3c's document for the xml file. Either returns cached reference * or parses the file and creates new document. * * @return the parsed document * @exception SAXException if there is a parsing error * @exception IOException if there is an I/O error */ public final Document getDocument () throws IOException, SAXException { Document doc = (Document)xmlDocument.get (); if (doc != null) return doc; synchronized (this) { doc = (Document)xmlDocument.get (); if (doc != null) return doc; status = STATUS_OK; try { doc = parsePrimaryFile (); } catch (SAXException e) { status = STATUS_ERROR; throw e; } catch (IOException e) { status = STATUS_ERROR; throw e; } // if (doc == null) // never // status = STATUS_ERROR; // store it in the cache xmlDocument = new SoftReference (doc); return doc; } } /** Clears the document. Called when the document file is changed. */ final void clearDocument () { xmlDocument.clear (); firePropertyChange (PROP_DOCUMENT, null, null); } /** @return one of STATUS_XXX constants */ public final int getStatus () { return status; } /* @return info. Returns null if file has no xmlinfo. */ public final Info getInfo () { infoTask.waitFinished(); if (info == null) return info; return (Info)info.clone(); } /* Sets info */ public final synchronized void setInfo (Info ii) throws IOException { setInfoImpl (ii); writeInfo(); } private final void setInfoImpl (Info ii) throws IOException { if (info == ii) return; if ((info != null) && info.equals (ii)) return; Info prevInfo = info; info = ii; if (info != null) { for (Iterator it = info.processorClasses(); it.hasNext(); ) { try { Class c = (Class)it.next(); Object o = c.newInstance (); Processor proc = (Processor)o; proc.attachTo (this); getCookieSet().add (proc); } catch (InstantiationException e) { throw new IOException (e.getClass().getName() + ": " + e.getMessage()); // NOI18N } catch (IllegalAccessException e) { throw new IOException (e.getClass().getName() + ": " + e.getMessage()); // NOI18N } } String iconBase = info.getIconBase(); if (iconBase != null) { infoTask.waitFinished(); updateIconBase (iconBase); } } firePropertyChange (PROP_INFO, prevInfo, info); } private void writeInfo () throws IOException { if (info == null) return; final FileObject primary = getPrimaryFile(); final FileObject parent = primary.getParent(); final org.openide.filesystems.FileSystem FS = parent.getFileSystem(); FS.runAtomicAction (new org.openide.filesystems.FileSystem.AtomicAction () { public void run () throws IOException { FileLock lock = null; OutputStream os = null; FileObject infoFO = FS.find (parent.getName(), primary.getName(), Loader.XMLINFO_EXT); if (infoFO == null) infoFO = parent.createData (primary.getName(), Loader.XMLINFO_EXT); try { lock = infoFO.lock (); os = infoFO.getOutputStream (lock); PrintWriter writer = new PrintWriter (os); info.write (writer); writer.close(); } finally { if (os != null) os.close (); if (lock != null) lock.releaseLock (); } } }); } /** Parses the primary file of this data object. * and provide different implementation. * * @return the document in the primary file * @exception IOException if error during parsing occures */ final Document parsePrimaryFile () throws IOException, SAXException { URL url = getPrimaryFile ().getURL (); return parse (url, errorHandler, false); } // Start of Utilities /** Provides access to internal XML parser. * This method takes URL. After successful finish the * document tree is returned. Used non validating parser. * * @param url the url to read the file from */ public static Document parse (URL url) throws IOException, SAXException { return parse (url, errorHandler, false); } /** Provides access to internal XML parser. * This method takes URL. After successful finish the * document tree is returned. Used non validating parser. * * @param url the url to read the file from * @param validate if true validating parser is used */ public static Document parse (URL url, boolean validate) throws IOException, SAXException { return parse (url, errorHandler, validate); } /** Provides access to internal XML parser. * This method takes URL. After successful finish the * document tree is returned. * * @param url the url to read the file from * @param eh error handler to notify about exception */ public static Document parse (URL url, ErrorHandler eh) throws IOException, SAXException { return parse (url, eh, false); } /** Provides access to internal XML parser. * This method takes URL. After successful finish the * document tree is returned. * * @param url the url to read the file from * @param eh error handler to notify about exception * @param validate if true validating parser is used */ public static Document parse (URL url, ErrorHandler eh, boolean validate) throws IOException, SAXException { Parser parser; com.sun.xml.tree.XmlDocumentBuilder builder; parser = createParser (validate); parser.setErrorHandler (eh); builder = new com.sun.xml.tree.XmlDocumentBuilder(); builder.setDisableNamespaces (true); builder.setParser (parser); parser.parse (createInputSource (url)); return builder.getDocument (); } /** Creates SAX parse that can be used to parse XML files. * @return sax parser */ public static Parser createParser () { return createParser (false); } /** Creates SAX parse that can be used to parse XML files. * @param validate if true validating parser is returned * @return sax parser */ public static Parser createParser (boolean validate) { Parser parser; if (validate) parser = new com.sun.xml.parser.ValidatingParser (true); else parser = new com.sun.xml.parser.Parser (); parser.setEntityResolver (entityResolver); return parser; } /** Creates empty DOM Document. */ public static Document createDocument () { return new com.sun.xml.tree.XmlDocument (); } /** Writes DOM Document to writer. */ public static void write (Document doc, Writer writer) throws IOException { if (doc instanceof com.sun.xml.tree.XmlDocument) { ((com.sun.xml.tree.XmlDocument)doc).write (writer); } else { throw new InternalError ("Unsupported DOM Document!"); // NOI18N } } /** Creates SAX InputSource for specified URL */ public static org.xml.sax.InputSource createInputSource (URL url) throws IOException { InputStream stream = url.openStream(); if (stream instanceof PushbackInputStream) stream = new DataInputStream (stream); // see com.sun.xml.parser.XmlReader line 192 org.xml.sax.InputSource retval = entityResolver.createInputSource (null, stream, false, url.getProtocol()); retval.setSystemId (url.toString ()); return retval; // return entityResolver.createInputSource (url, true); // previous version // not correct for mime type "text/xml" with url protocol "http" (!"file") // NOI18N // -> every time created ASCII reader // see com.sun.xml.parser.Resolver line 221 and com.sun.xml.parser.XmlReade line 109 } /** * Registers the given public ID as corresponding to a particular * URI, typically a local copy. This URI will be used in preference * to ones provided as system IDs in XML entity declarations. This * mechanism would most typically be used for Document Type Definitions * (DTDs), where the public IDs are formally managed and versioned. * * <P> Any created parser use global entity resolver and you can * register its catalog entry. * * @param publicId The managed public ID being mapped * @param uri The URI of the preferred copy of that entity */ public static void registerCatalogEntry (String publicId, String uri) { entityResolver.registerCatalogEntry (publicId, uri); } /** * Registers a given public ID as corresponding to a particular Java * resource in a given class loader, typically distributed with a * software package. This resource will be preferred over system IDs * included in XML documents. This mechanism should most typically be * used for Document Type Definitions (DTDs), where the public IDs are * formally managed and versioned. * * <P> If a mapping to a URI has been provided, that mapping takes * precedence over this one. * * <P> Any created parser use global entity resolver and you can * register its catalog entry. * * @param publicId The managed public ID being mapped * @param resourceName The name of the Java resource * @param loader The class loader holding the resource, or null if * it is a system resource. */ public static void registerCatalogEntry (String publicId, String resourceName, ClassLoader loader) { entityResolver.registerCatalogEntry (publicId, resourceName, loader); } // end of utilities // class ErrorPrinter static class ErrorPrinter implements org.xml.sax.ErrorHandler { private void message (final String level, final org.xml.sax.SAXParseException e) { TopManager.getDefault ().notifyException (new Throwable () { public String getMessage () { return new String (MessageFormat.format (NbBundle.getBundle (XMLDataObject.class).getString ("PROP_XmlMessage"), new Object [] { level, e.getMessage(), (e.getSystemId() == null ? "" : e.getSystemId()), // NOI18N e.getLineNumber()+"", // NOI18N e.getColumnNumber()+"" // NOI18N })); } }); } public void error (org.xml.sax.SAXParseException e) { message (NbBundle.getBundle (XMLDataObject.class).getString ("PROP_XmlError"), e); } public void warning (org.xml.sax.SAXParseException e) { message (NbBundle.getBundle (XMLDataObject.class).getString ("PROP_XmlWarning"), e); } public void fatalError (org.xml.sax.SAXParseException e) { message (NbBundle.getBundle (XMLDataObject.class).getString ("PROP_XmlFatalError"), e); } } // end of inner class ErrorPrinter /** This class has to be implemented by all processors in the * xmlinfo file. It is cookie, so after parsing such class is instantiated * and put into data objects cookie set. */ public static interface Processor extends Node.Cookie { /** When the XMLDataObject creates new instance of the processor, * it uses this method to attach the processor to the data object. * * @param xmlDO XMLDataObject */ public void attachTo (XMLDataObject xmlDO); } /** Parser for XML info. */ private final class InfoParser extends HandlerBase implements Runnable, FileChangeListener { /** the name of info tag */ private static final String TAG_INFO = "info"; // NOI18N /** the name of processor tag */ private static final String TAG_PROCESSOR = "processor"; // NOI18N /** the class attribute */ private static final String ATT_PROCESSOR_CLASS = "class"; // NOI18N /** icon tag */ private static final String TAG_ICON = "icon"; // NOI18N /** base attribute */ private static final String ATT_ICON_BASE = "base"; // NOI18N /** file object to parse */ private FileObject fileObject; private Info tempInfo; /** Changes the info file that starts new parsing. */ public synchronized void setInfoFile (FileObject fo) { // synchronized to allow only one parsing to be running at one time. // wait till previous parsing finishes infoTask.waitFinished (); fileObject = fo; CookieSet cs = new CookieSet (); cs.add (editor); setCookieSet (cs); if (fileObject == null) return; // start new parsing infoTask = XML_RP.post ( this, 0, Thread.NORM_PRIORITY - 1 ); } public void startDocument () { tempInfo = new Info(); } public void endDocument () { try { XMLDataObject.this.setInfoImpl (tempInfo); } catch (Exception e) { TopManager.getDefault().notifyException (e); } } /** Accepts module item */ public void startElement (String name, AttributeList attr) { if (name.equals (TAG_PROCESSOR)) { String className = attr.getValue (ATT_PROCESSOR_CLASS); if (className != null) { try { className = org.openide.util.Utilities.translate(className); Class c = Class.forName (className, true, TopManager.getDefault().systemClassLoader ()); tempInfo.addProcessorClass (c); } catch (Exception e) { TopManager.getDefault ().notifyException (e); } } return; } if (name.equals (TAG_ICON)) { String file = attr.getValue (ATT_ICON_BASE); tempInfo.setIconBase (file); return; } } /** Starts the parsing. */ public void run () { try { Parser p = createParser (); p.setDocumentHandler (this); p.setErrorHandler (errorHandler); p.parse (createInputSource (fileObject.getURL())); } catch (SAXException ex) { TopManager.getDefault ().notifyException (ex); } catch (IOException ex) { TopManager.getDefault ().notifyException (ex); } } public void fileFolderCreated (FileEvent fe) { // not interesting } public void fileDataCreated (FileEvent fe) { FileObject fo = fe.getFile (); if ( fo.hasExt (Loader.XMLINFO_EXT) && fo.getName ().equals (getPrimaryFile ().getName ()) ) { // new info file created => force it to be reparsed setInfoFile (fo); } } /** Fired when a file is changed. * @param fe the event describing context where action has taken place */ public void fileChanged (FileEvent fe) { if (fe.getFile().equals (fileObject)) { // repase the file setInfoFile (fe.getFile ()); } else { if (getPrimaryFile ().equals (fe.getFile ())) { // the main file changed clearDocument (); } } } public void fileDeleted (FileEvent fe) { if (fe.getFile().equals (fileObject)) { // repase the file setInfoFile (null); } } public void fileRenamed (FileRenameEvent fe) { // the same behaviour as when the file is deleted fileDeleted (fe); } public void fileAttributeChanged (FileAttributeEvent fe) { } } // end of InfoParser /** The DataLoader for XmlDataObjects. * This class is final only for performance reasons, * can be happily unfinaled if desired. */ static class Loader extends MultiFileLoader { /** Extension constants */ static final String XML_EXT = "xml"; // NOI18N static final String XMLINFO_EXT = "xmlinfo"; // NOI18N static final long serialVersionUID =3917883920409453930L; /** Creates a new XMLDataLoader */ public Loader () { super (XMLDataObject.class); } /** Initialize XMLDataLoader: name, actions, ... */ protected void initialize () { setDisplayName( NbBundle.getBundle (XMLDataObject.class).getString ("PROP_XmlLoader_Name") ); setActions(new SystemAction[] { SystemAction.get(OpenAction.class), null, SystemAction.get(CutAction.class), SystemAction.get(CopyAction.class), SystemAction.get(PasteAction.class), null, SystemAction.get(DeleteAction.class), SystemAction.get(RenameAction.class), null, SystemAction.get(SaveAsTemplateAction.class), null, SystemAction.get(ToolsAction.class), SystemAction.get(PropertiesAction.class) }); } /** For a given file finds a primary file. * @param fo the file to find primary file for * * @return the primary file for the file or null if the file is not * recognized by this loader */ protected FileObject findPrimaryFile (FileObject fo) { if (XML_EXT.equals(fo.getExt())) { return fo; } if ("tld".equals(fo.getExt())) { // NOI18N return fo; // JSP Tag Library Descriptor } if (XMLINFO_EXT.equals(fo.getExt())) { return FileUtil.findBrother (fo, XML_EXT); } // not recognized return null; } /** Creates the right data object for given primary file. * It is guaranteed that the provided file is realy primary file * returned from the method findPrimaryFile. * * @param primaryFile the primary file * @return the data object for this file * @exception DataObjectExistsException if the primary file already has data object */ protected MultiDataObject createMultiObject (FileObject primaryFile) throws DataObjectExistsException { return new XMLDataObject (primaryFile, this); } /** Creates the right primary entry for given primary file. * * @param primaryFile primary file recognized by this loader * @return primary entry for that file */ protected MultiDataObject.Entry createPrimaryEntry (MultiDataObject obj, FileObject primaryFile) { return new FileEntry (obj, primaryFile); } /** Creates right secondary entry for given file. The file is said to * belong to an object created by this loader. * * @param secondaryFile secondary file for which we want to create entry * @return the entry */ protected MultiDataObject.Entry createSecondaryEntry (MultiDataObject obj, FileObject secondaryFile) { return new FileEntry (obj, secondaryFile); } } /** * Representation of xmlinfo file */ public static final class Info implements Cloneable { /** * @associates Class */ Set processors; String iconBase; /** Create info */ public Info () { processors = new HashSet (7); iconBase = null; } public Object clone () { Info ii = new Info(); for (Iterator it = processors.iterator(); it.hasNext();) { Class proc = (Class)it.next(); ii.processors.add (proc); } ii.iconBase = iconBase; return ii; } /** Add processor class to info. */ public void addProcessorClass (Class proc) { if (!Processor.class.isAssignableFrom (proc)) throw new IllegalArgumentException(); processors.add (proc); } /** Remove processor class from info. * @return true if removed */ public boolean removeProcessorClass (Class proc) { return processors.remove (proc); } public Iterator processorClasses () { return processors.iterator(); } /** Set icon base */ public void setIconBase (String base) { iconBase = base; } /** @return icon base */ public String getIconBase () { return iconBase; } /** Write specified info to writer */ public void write (Writer writer) throws IOException { writer.write ("<?xml version=\"1.0\"?>\n\n"); // NOI18N writer.write (MessageFormat.format ("<!DOCTYPE {0} PUBLIC \"{1}\" \"\">\n\n", // NOI18N new Object [] { InfoParser.TAG_INFO, XMLINFO_DTD_PUBLIC_ID })); writer.write (MessageFormat.format ("<{0}>\n", // NOI18N new Object [] { InfoParser.TAG_INFO })); for (Iterator it = processors.iterator(); it.hasNext();) writer.write (MessageFormat.format (" <{0} {1}=\"{2}\" />\n", // NOI18N new Object [] { InfoParser.TAG_PROCESSOR, InfoParser.ATT_PROCESSOR_CLASS, ((Class)it.next()).getName() })); if (iconBase != null) writer.write (MessageFormat.format (" <{0} {1}=\"{2}\" />\n", // NOI18N new Object [] { InfoParser.TAG_ICON, InfoParser.ATT_ICON_BASE, iconBase })); writer.write (MessageFormat.format ("</{0}>\n", // NOI18N new Object [] { InfoParser.TAG_INFO })); } } // end of inner class XMLInfoCreator } /* * Log * 37 Gandalf 1.36 1/16/00 Libor Kramolis * 36 Gandalf 1.35 1/13/00 Ian Formanek NOI18N * 35 Gandalf 1.34 1/13/00 Jesse Glick All data loaders now * public for serialization. * 34 Gandalf 1.33 1/12/00 Ian Formanek NOI18N * 33 Gandalf 1.32 12/3/99 Petr Jiricka Added extension for JSP * tag library descriptor * 32 Gandalf 1.31 11/26/99 Patrik Knakal * 31 Gandalf 1.30 11/25/99 Libor Kramolis * 30 Gandalf 1.29 11/11/99 Libor Kramolis * 29 Gandalf 1.28 11/5/99 Jaroslav Tulach WeakListener has now * registration methods. * 28 Gandalf 1.27 10/22/99 Ian Formanek NO SEMANTIC CHANGE - Sun * Microsystems Copyright in File Comment * 27 Gandalf 1.26 10/1/99 Libor Kramolis * 26 Gandalf 1.25 9/30/99 Libor Kramolis * 25 Gandalf 1.24 9/30/99 Libor Kramolis Info updated * 24 Gandalf 1.23 9/29/99 Libor Kramolis xml parser changed & new * utils * 23 Gandalf 1.22 9/2/99 Libor Kramolis * 22 Gandalf 1.21 8/9/99 Libor Kramolis set * 21 Gandalf 1.20 8/6/99 Jaroslav Tulach parse (URL, * ErrorHandler) * 20 Gandalf 1.19 7/30/99 Libor Kramolis * 19 Gandalf 1.18 7/22/99 Libor Kramolis * 18 Gandalf 1.17 7/21/99 Jaroslav Tulach MultiDataObject can mark * easily mark secondary entries in constructor as belonging to the * object. * 17 Gandalf 1.16 7/20/99 Libor Kramolis * 16 Gandalf 1.15 7/19/99 Ian Formanek Info parsing in own * request processor * 15 Gandalf 1.14 7/19/99 Libor Kramolis * 14 Gandalf 1.13 7/16/99 Jaroslav Tulach Allows subclasses to * have own parser for the primary file. * 13 Gandalf 1.12 7/9/99 Libor Kramolis * 12 Gandalf 1.11 7/8/99 Jesse Glick Context help. * 11 Gandalf 1.10 7/3/99 Libor Kramolis * 10 Gandalf 1.9 7/2/99 Jaroslav Tulach Added PROP_DOCUMENT that * is fired when the document is changed. * 9 Gandalf 1.8 6/24/99 Ian Formanek Updated to better handle * existing code for changes introduced in last checkin * 8 Gandalf 1.7 6/22/99 Libor Kramolis * 7 Gandalf 1.6 6/9/99 Ian Formanek Fixed resources for * package change * 6 Gandalf 1.5 6/9/99 Ian Formanek ToolsAction * 5 Gandalf 1.4 6/8/99 Ian Formanek Minor changes * 4 Gandalf 1.3 6/8/99 Ian Formanek ---- Package Change To * org.openide ---- * 3 Gandalf 1.2 6/4/99 Libor Kramolis * 2 Gandalf 1.1 5/26/99 Ian Formanek changed incorrect usage * of getBundle * 1 Gandalf 1.0 5/24/99 Jaroslav Tulach * $ */